# dest_machine.py

r'''These are the dest_machine classes.

This is the sole interface point from the rest of the program to the output
code (which is the code that knows about the target CNC machine).
'''

from __future__ import division

import string
import itertools
import sys      # for debugging

from ot_simulator.template.param_funs import get_param

class dest_machine(object):
    r'''This object orchestrates converting and outputing to the CNC machine.

    It uses two helper objects:

    transformer:
        does the coordinate system transformation to the target CNC machine.
        This lets us share the transformation routines between multiple
        machines with the same coordinate systems.
    outputer:
        outputs instructions to the machine.  This deals with the differences
        between automatic operation (through generation of a file that the CNC
        machine runs) and manual operation (through some kind of "drip mode"
        operation).
    '''

    def __init__(self, transformer, outputer):
        self.transformer = transformer
        self.outputer = outputer

    def start_cut(self, params, line_no = None):
        r'''A "cut", as in roughing cut or finishing cut.

        This emcompasses one or more independent passes, where each pass
        varies one variable to move the tool along one of the axes.  If the
        cut is shaping a 3D object, it will emcompass two nested passes, each
        varying a different axis.  In this case, both passes are included in
        the same "cut".

        For G-Code machines, the cut initializes the machine (if it's the
        first cut), outputs a line number (Nnnnn) if the lineno parameter has
        changed, and outputs a tool change sequence if the tool parameter has
        changed.
        '''
        pass

    def end_cut(self, params):
        pass

    def start_pass(self, params):
        r'''A "pass" is one continuous pass of the cutting tool.

        Rapid movements are generally required between passes to reposition
        the tool for the next pass.
        '''
        pass

    def end_pass(self, params):
        pass

    def rapid_to(self, pos, params):
        self.prepare_rapid(params)
        self.goto(self.transformer.convert_pos(pos))
        self.finish_rapid(params)

    def rapid(self, work_to_tool_transform, params):
        self.prepare_rapid(params)
        self.goto(self.transformer.convert(work_to_tool_transform))
        self.finish_rapid(params)

    def with_offset(self, work_to_tool_transform, params):
        return work_to_tool_transform

    def output_point(self, work_to_tool_transform, params):
        self.prepare_output(params)
        self.goto(self.transformer.convert(work_to_tool_transform))
        self.finish_output(params)

    def prepare_rapid(self, params):
        pass

    def finish_rapid(self, params):
        pass

    def prepare_output(self, params):
        pass

    def finish_output(self, params):
        pass

    def goto(self, positions):
        pass

    def open(self, filename):
        self.outputer.open(filename)

    def close(self):
        self.outputer.close()

# G-code initialization for the Tomach at GWIZ Fab Lab)
#
# This sets inches, constant velocity, absolute distance, )
#           enable speed and feed override, feed in/min)
#
# Uses $name and $comments string template codes: )
#
# Codes that have been deleted:
# G61                                (13 Exact stop mode -- vs G64)
# M49                                (   Disable speed and feed override)
# G95                                ( 5? Feed per spindle rev mode)
# G92.2                              (   Reset all axis offsets)
#
G_code_start = string.Template(
r"""O$name
$comments
G40                                  ( 7 Cancel cutter radius compensation)
G49                                  ( 8 Cancel tool length offset)
G50                                  (   Reset all scale factors to 1.0)
G52 X0 Y0 Z0 A0                      (   Temporary coordinate system offsets)
G69                                  (   Cancel Coordinate system rotation)
G80                                  ( 1 Cancel motion modes)
M9                                   (   All coolant off)
G17                                  ( 2 XY Plane Select, for arcs and canned)
G20                                  ( 6 Inch unit)
G64                                  (13 Constant Velocity mode)
G90                                  ( 3 Absolute distance mode)
M48                                  (   Enable speed and feed override)
G94                                  ( 5 Feed per minute mode)
G54                                  (   Select work offset #1)
""")


# G-code job termination for the Tomach at GWIZ Fab Lab
#
# Uses no string template codes:
#
# Codes that have been deleted:
# G28                               ( 0 Return Home, via X/Y/Z/A if specified)
#
G_code_stop = \
r"""M5                                  (   Stop spindle rotation)
M30                                 (   End program and "rewind")
"""


# G-code tool change for the Tomach at GWIZ Fab Lab
#
# Uses $tool, $tool_desc and $speed string template codes:
#
#  $tool:  Tool number to change to)
#  $tool_desc: text for operator message)
#  $speed: Spindle speed rpm)
#
# Codes that have been deleted:
# M5                                (   Stop spindle rotation)
# G28                               ( 0 Return Home, via X/Y/Z/A if specified)
#
G_code_change = string.Template(
r"""T$tool M6 G43 H$tool                (MSG, $tool_desc)
M3 S$speed                          (   Turn spindle on clockwise)
""")

class g_code(dest_machine):
    r'''Base class for all G-code dest_machine classes.

    The spindle will start positioned clear of the work at the ``clear1``
    setting (e.g., "Y6.0"), which is before the tool offset has been set on
    the machine.  :meth:`change_tool` leaves the tool at the ``clear2``
    setting after the tool change is complete.  The ``clear2`` setting is
    after the tool offset has been set on the machine.

    Then each pass starts with:

    - rapid to a fixed X,Z,C position (``pos1``)
    - rapid to the desired Y position (``pos2``) (optional).
    - rapid close to the work (based on a work_to_tool_transform)
    - output the standard G1 points to cut
    - rapid to a fixed ``out`` position.
    '''

    G_code_start  = G_code_start        # string.Template
    G_code_change = G_code_change       # string.Template
    G_code_stop   = G_code_stop
    G_code_break  = "M1\n"
    G_code_lineno = "N{lineno} "
    G_code_offset = "G92 {0}\n"
    G_code_rapid  = "G0 "
    G_code_line   = "G1 "
    G_code_feed   = " F{feed:.4f}"

    def __init__(self, transformer, outputer, name=None, comments=()):
        dest_machine.__init__(self, transformer, outputer)
        self.name = name
        self.comments = list(comments)
        self.last_cnc_angles = {}
        self.last_line_no = None
        self.state = 'uninitialized'

    def add_comments(self, comments):
        self.comments.extend(comments)

    def g_code_start(self, params):
        r'''Currently doesn't use any params.
        '''
        if self.state != 'uninitialized':
            raise AssertionError(
                    "g_code.g_code_start called in state " + self.state)
        self.outputer.write(
          self.G_code_start.substitute(
            params,
            name=self.name,
            comments='\n'.join('({0})'.format(c) for c in self.comments)))
        if 'start_gcode' in params:
            start_gcode = get_param(params, 'start_gcode')
            if isinstance(start_gcode, (str, unicode)):
                self.outputer.write(start_gcode + '\n')
            else:
                self.outputer.write('\n'.join(start_gcode))
        self.state = 'started'
        self.tool = None
        self.g_mode = None
        self.feed_rate = None

    def g_code_stop(self):
        self.outputer.write(self.G_code_stop)
        self.state = 'uninitialized'

    def change_tool(self, params):
        self.rapid_to(get_param(params, 'clear1'), params)
        self.outputer.write(self.G_code_change.substitute(params))
        self.tool = get_param(params, 'tool')
        self.g_mode = None
        self.feed_rate = None
        self.rapid_to(get_param(params, 'clear2'), params)

    def start_cut(self, params, line_no=None):
        if self.state == 'uninitialized':
            self.g_code_start(params)
        if self.state != 'started':
            raise AssertionError(
                    "g_code.start_cut called in state " + self.state)
        if line_no is not None:
            if line_no != self.last_line_no:
                if self.last_line_no is not None:
                    self.outputer.write(self.G_code_break)
                self.outputer.write(self.G_code_lineno.format(lineno=line_no))
                print >> sys.stderr, "N{0}".format(line_no)
                self.last_line_no = line_no
                self.g_mode = None
                self.feed_rate = None
        if self.tool != get_param(params, 'tool'):
            self.change_tool(params)
        self.state = 'cut_started'

    def end_cut(self, params):
        self.state = 'started'

    def start_pass(self, params):
        if self.state != 'cut_started':
            raise AssertionError(
                    "g_code.start_pass called in state " + self.state)
        if 'pos1' in params:
            self.rapid_to(get_param(params, 'pos1'), params)
        if 'pos2' in params:
            self.rapid_to(get_param(params, 'pos2'), params)
        self.state = 'running'

    def end_pass(self, params):
        self.rapid_to(get_param(params, 'out'), params)
        axis_reset = []
        for axis, angle in sorted(self.last_cnc_angles.items()):
            new_angle = angle % 360
            if abs(new_angle - angle) > 0.5e-3:
                axis_reset.append("{0}{1:.3f}".format(axis, new_angle))
                self.last_cnc_angles[axis] = new_angle
        if axis_reset and self.G_code_offset:
            self.outputer.write(self.G_code_offset.format(' '.join(axis_reset)))
        self.state = 'cut_started'

    def prepare_rapid(self, params):
        if self.g_mode != "G0":
            self.outputer.write(self.G_code_rapid)
            self.g_mode = "G0"
        self.feed_rate = None

    def finish_rapid(self, params):
        self.outputer.write('\n')

    def prepare_output(self, params):
        if self.g_mode != "G1":
            self.outputer.write(self.G_code_line)
            self.g_mode = "G1"

    def finish_output(self, params):
        feed = get_param(params, 'feed')
        if self.feed_rate != feed:
            self.outputer.write(self.G_code_feed.format(feed=feed))
            self.feed_rate = feed
        self.outputer.write('\n')

    def goto(self, positions):
        self.outputer.write(
          ' '.join(itertools.chain(
                     ("{0}{1:.4f}".format(axis, positions[axis])
                        for axis in "XYZ" if axis in positions),
                     ("{0}{1:.3f}".format(axis,
                                          self.convert_angle(axis,
                                                             positions[axis]))
                        for axis in "ABC" if axis in positions))))

    def convert_angle(self, axis, value):
        if axis in self.last_cnc_angles:
            last_value = self.last_cnc_angles[axis]
            value += round((last_value - value) / 360.0) * 360.0
        self.last_cnc_angles[axis] = value
        return value

    def close(self):
        self.g_code_stop()
        super(g_code, self).close()

Axis_convert = dict(zip("XYZ", "ABC"))

